package com.danicsoft.daide.command;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.danicsoft.daide.token.IntegerToken;
import com.danicsoft.daide.token.TextToken;
import com.danicsoft.daide.token.Token;
import com.danicsoft.daide.token.TokenGroup;
import com.danicsoft.daide.token.TokenMap;
import com.danicsoft.daide.token.TypeClass;

/**
 * <p>
 * This class handles output and input from an <code>IOTarget</code> to a
 * <code>TokenGroup</code> or visa versa. The basic idea is that any input or
 * output is handled by this class. Then, if it is input it handles it one way,
 * and if it is output, it handles it another. This works via subclasses. There
 * is a sub class for input and a subclass for output. This is hidden from any
 * user of TokenIO however. They only know that they are doing either input or
 * output, depending on the context, and they don't know the context. Basically,
 * this class handles the context, so the other classes don't need to worry
 * about it.
 * </p>
 * <p>
 * If you are using this class, and your name is not Daniel Yule, you need to
 * seriously re-examine why you are doing so. I'm not claiming to be some kind
 * of super genius, in fact I would go so far as to say that if I were, I should
 * be able to write code that anyone can use throughout the project. No, its
 * just that the system I've dreamed up for IO is fairly complex and I have
 * little to no faith in my explanatory abilities.
 * </p>
 * 
 * @author Daniel Yule (daniel.yule@gmail.com)
 * @version 1.0
 */
public abstract class TokenIO {

	/**
	 * The subclass of TokenIO that deals with input. That is, it reads from a
	 * <code>TokenGroup</code> and writes in some way to an
	 * <code>IOTarget</code>, or various other data types.
	 * 
	 * @author Daniel Yule (daniel.yule@gmail.com)
	 * @version 1.0
	 * @see TokenIO
	 */
	private static class TokenInput extends TokenIO {

		/**
		 * This method parses a TokenGroup that consists of a command and sets
		 * the field specified as this newly parsed <code>Command</code>.
		 * 
		 * @param parentGroup
		 *        The <code>TokenGroup</code> that contains this command. The
		 *        next token of this group should be a <code>TokenGroup</code>
		 *        whose first token must be a <code>StandardToken</code> of
		 *        type <code>COMMAND</code>. Following that are the rest of
		 *        the arguments for the command exactly as they woud be if we
		 *        were loading from a top level command.
		 * @param commandField
		 *        A field corresponding to a <code>Command</code> field on the
		 *        target specified. Note that it must be public.
		 * @param target
		 *        An IOTarget that contains the commandField above.
		 * @throws ParseException
		 *         If a group without a proper command was found, or if there
		 *         was an error parsing the command.
		 * @see TokenIO#doCommand(TokenGroup, Command, IOTarget)
		 */
		@Override
		public Command doCommand(TokenGroup parentGroup, Command commandField, IOTarget target) throws ParseException {
			try {
				TokenGroup commandTokens = (TokenGroup) parentGroup.next();
				return CommandParser.parse(commandTokens);
			}
			catch (ClassCastException e) {
				throw new ParseException("Expected a group with a command in it.");
			}
		}

		/**
		 * Evaluates whether or not to proceed based on the presence or lack of
		 * presence of the specified token. If that token is present, the next
		 * token returned from <code>parentGroup</code> ought to be the token
		 * following. Otherwise the next token will be the token tested and
		 * found not to be the correct one.
		 * 
		 * @param parentGroup
		 *        The <code>TokenGroup</code> that contains the token to be
		 *        examined. We will look at the next <code>Token</code>, if
		 *        there is one.
		 * @param conditionalToken
		 *        The <code>Token</code> to check for in the group.
		 * @param conditionalExpression
		 *        Ignored, for input. However, whatever is passed as
		 *        <code>conditionalExpression</code> should be assigned to the
		 *        return value;
		 * @return true if the token was present, false otherwise.
		 * @see com.danicsoft.daide.command.TokenIO#doConditional(com.danicsoft.daide.token.TokenGroup,
		 *      com.danicsoft.daide.token.Token, boolean)
		 */
		@Override
		public boolean doConditional(TokenGroup parentGroup, Token conditionalToken, boolean conditionalExpression) {
			if (parentGroup.hasNext()) {
				if (parentGroup.next().equals(conditionalToken)) {

					return true;
				}

				parentGroup.previous();
			}
			return false;
		}

		/**
		 * This generates a <code>Map</code> keyed on the next
		 * <code>Token</code> in <code>parentGroup</code> and valued on an
		 * <code>IOTarget</code> generated from the remaining
		 * <code>Tokens</code>.
		 * 
		 * @param parentGroup
		 *        A <code>TokenGroup</code> containing the <code>Token</code>s
		 *        to be parsed.
		 * @param tokenSet
		 *        A non-null <code>Map</code> which maps from
		 *        <code>Token</code>s to <code>IOTarget</code>s.
		 * @param exampleObject
		 *        An object from which new instances of the same class can be
		 *        generated. This is used to create the <code>IOTarget</code>s
		 *        that are put in the map.
		 * @throws ParseException
		 *         If there was a problem parsing the tokens making up the
		 *         value.
		 * @see com.danicsoft.daide.command.TokenIO#doHashOnFirst(com.danicsoft.daide.token.TokenGroup,
		 *      java.util.Map, IOTarget, TypeClass)
		 */
		@SuppressWarnings("unchecked")
		@Override
		public <E extends IOTarget> Map<Token, E> doHashOnFirst(TokenGroup parentGroup, Map<Token, E> tokenSet, E exampleObject, TypeClass tokenType) throws ParseException {
			Map<Token, E> mySet = new HashMap<Token, E>();
			for (Token groupToken : parentGroup) {
				TokenGroup hashGroup = (TokenGroup) groupToken;
				Token firstTok = hashGroup.next().clone();
				if (!tokenType.isOfType(firstTok)) {
					throw new ParseException("Expected type " + tokenType);
				}
				E thisObj = (E) exampleObject.newInstance();
				thisObj.doIO(this, hashGroup);
				mySet.put(firstTok, thisObj);
			}
			return mySet;

		}

		@SuppressWarnings("unchecked")
		@Override
		public <E extends Token> Map<E, List<E>> doHashOnFirstToken(TokenGroup parentGroup, Map<E, List<E>> tokenSet, TypeClass firstType, TypeClass remainingTypes) throws ParseException {
			Map<E, List<E>> mySet = new HashMap<E, List<E>>();
			for (Token groupToken : parentGroup) {
				TokenGroup hashGroup = (TokenGroup) groupToken;
				Token firstTok = hashGroup.next().clone();
				if (!firstType.isOfType(firstTok)) {
					throw new ParseException("Expected type " + firstType);
				}
				List<E> values = new LinkedList<E>();
				for (Token tok : hashGroup) {
					if (!remainingTypes.isOfType(tok)) {
						throw new ParseException("Expected type " + remainingTypes);
					}
					values.add((E) tok.clone());
				}
				mySet.put((E) firstTok, values);
			}
			return mySet;

		}

		@Override
		public int doInteger(TokenGroup parentGroup, int integerField) throws ParseException {
			try {
				IntegerToken tToken = (IntegerToken) parentGroup.next();
				return tToken.getData();
			}
			catch (ClassCastException e) {
				throw new ParseException("Expected an integer token.");
			}

		}

		@SuppressWarnings("unchecked")
		@Override
		public <E extends IOTarget> List<E> doList(TokenGroup parentGroup, List<E> list, E exampleObject) throws ParseException {
			List<E> myList = new ArrayList<E>();
			try {
				while (parentGroup.hasNext()) {
					IOTarget thisTarget = exampleObject.newInstance();
					thisTarget.doIO(this, parentGroup);
					myList.add((E) thisTarget);
				}
			}
			catch (ClassCastException e) {
				throw new ParseException("Expected a group.");
			}

			return myList;

		}

		@Override
		public String doText(TokenGroup parentGroup, String textField) throws ParseException {
			try {
				TextToken tToken = (TextToken) parentGroup.next();
				return tToken.getData();
			}
			catch (ClassCastException e) {
				throw new ParseException("Expected a text token.");
			}

		}

		@Override
		public Token doToken(TokenGroup parentGroup, Token tokenField, TypeClass tokenType) throws ParseException {
			Token rToken = parentGroup.next().clone();
			if (!tokenType.isOfType(rToken)) {
				throw new ParseException("Expected type " + tokenType);
			}
			return rToken;

		}

		@SuppressWarnings("unchecked")
		@Override
		public <E extends Token> List<E> doTokenList(TokenGroup parentGroup, List<E> list, TypeClass tokenType) throws ParseException {
			List<E> myList = new ArrayList<E>();
			try {
				for (Token tok : parentGroup) {
					if (!tokenType.isOfType(tok)) {
						throw new ParseException("Expected type " + tokenType);
					}
					myList.add((E) tok.clone());
				}
			}
			catch (ClassCastException e) {
				throw new ParseException("Unexpected token encountered");
			}
			return myList;

		}

		@Override
		public TokenGroup subGroup(TokenGroup parentGroup) {
			return (TokenGroup) parentGroup.next();
		}

		/**
		 * Moves ahead only if there are more tokens to evaluate in the
		 * <code>TokenGroup</code>.
		 * 
		 * @param parentGroup
		 *        The <code>TokenGroup</code> to look for more tokens in.
		 * @param conditionalExpression
		 *        Ignored for input.
		 * @return True if there are more tokens in <code>parentGroup</code>
		 * @see com.danicsoft.daide.command.TokenIO#doIfMore(com.danicsoft.daide.token.TokenGroup,
		 *      boolean)
		 */
		@Override
		public boolean doIfMore(TokenGroup parentGroup, boolean conditionalExpression) {
			return parentGroup.hasNext();
		}

		/**
		 * Continues only if there is a group next in the given tokenGroup.
		 * 
		 * @param parentGroup
		 *        The group to check
		 * @param conditionalExpression
		 *        Ignored for input
		 * @return True if the next token will be a group.
		 * @see com.danicsoft.daide.command.TokenIO#doIfGroup(com.danicsoft.daide.token.TokenGroup,
		 *      boolean)
		 */
		@Override
		public boolean doIfGroup(TokenGroup parentGroup, boolean conditionalExpression) {
			Token nextToken = parentGroup.next();
			parentGroup.previous();
			return nextToken.isGroup();
		}

	}

	private static class TokenOutput extends TokenIO {

		@Override
		public Command doCommand(TokenGroup parentGroup, Command commandField, IOTarget target) throws ParseException {
			try {
				TokenGroup commandGroup = doOutput(commandField);
				parentGroup.add(commandGroup);
				return commandField;
			}
			catch (ClassCastException e) {
				throw new ParseException("Expected a command field.");
			}
		}

		@Override
		public boolean doConditional(TokenGroup parentGroup, Token conditionalToken, boolean conditionExpression) {
			if (conditionExpression) {
				parentGroup.add(conditionalToken.clone());
			}
			return conditionExpression;
		}

		@Override
		public <E extends IOTarget> Map<Token, E> doHashOnFirst(TokenGroup parentGroup, Map<Token, E> tokenSet, E exampleObject, TypeClass tokenType) throws ParseException {
			for (Object firstTok : tokenSet.keySet()) {
				TokenGroup thisGroup = new TokenGroup();
				thisGroup.add(((Token) firstTok).clone());
				IOTarget thisObj = tokenSet.get(firstTok);
				thisObj.doIO(this, thisGroup);
				parentGroup.add(thisGroup);
			}
			return tokenSet;

		}

		@Override
		public <E extends Token> Map<E, List<E>> doHashOnFirstToken(TokenGroup parentGroup, Map<E, List<E>> tokenSet, TypeClass firstType, TypeClass remainingTypes) throws ParseException {
			for (E firstTok : tokenSet.keySet()) {
				TokenGroup thisGroup = new TokenGroup();
				thisGroup.add(firstTok.clone());
				List<E> tokenList = tokenSet.get(firstTok);
				for (E tok : tokenList) {
					thisGroup.add(tok.clone());
				}
				parentGroup.add(thisGroup);
			}

			return tokenSet;

		}

		@Override
		public int doInteger(TokenGroup parentGroup, int integerField) throws ParseException {
			parentGroup.add(new IntegerToken(integerField));
			return integerField;

		}

		@Override
		public <E extends IOTarget> List<E> doList(TokenGroup parentGroup, List<E> list, E exampleObject) throws ParseException {
			for (E target : list) {
				target.doIO(this, parentGroup);
			}
			return list;

		}

		@Override
		public String doText(TokenGroup parentGroup, String textField) throws ParseException {

			parentGroup.add(new TextToken(textField));
			return textField;

		}

		@Override
		public Token doToken(TokenGroup parentGroup, Token tokenField, TypeClass tokenType) throws ParseException {

			parentGroup.add(tokenField.clone());
			return tokenField;

		}

		@Override
		public <E extends Token> List<E> doTokenList(TokenGroup parentGroup, List<E> list, TypeClass tokenType) throws ParseException {
			for (E obj : list) {
				parentGroup.add(obj.clone());
			}

			return list;

		}

		@Override
		public TokenGroup subGroup(TokenGroup parentGroup) {
			TokenGroup sGroup = new TokenGroup();
			parentGroup.add(sGroup);
			return sGroup;
		}

		/**
		 * Continues only if the conditional expression is true.
		 * 
		 * @param parentGroup
		 *        A <code>TokenGroup</code> that is ignored for output.
		 * @param conditionalExpression
		 *        The expression that must be true in order to continue.
		 * @return True if conditionExpression is true;
		 * @see com.danicsoft.daide.command.TokenIO#doIfMore(com.danicsoft.daide.token.TokenGroup,
		 *      boolean)
		 */
		@Override
		public boolean doIfMore(TokenGroup parentGroup, boolean conditionalExpression) {
			return conditionalExpression;
		}

		/**
		 * Continues only if the conditional expression is true.
		 * 
		 * @param parentGroup
		 *        A <code>TokenGroup</code> that is ignored for output.
		 * @param conditionalExpression
		 *        The expression that must be true in order to continue.
		 * @return True if conditionExpression is true;
		 * @see com.danicsoft.daide.command.TokenIO#doIfGroup(com.danicsoft.daide.token.TokenGroup,
		 *      boolean)
		 */
		@Override
		public boolean doIfGroup(TokenGroup parentGroup, boolean conditionalExpression) {
			return conditionalExpression;
		}

	}

	/**
	 * The singleton <code>TokenIO</code> we will be using for input.
	 */
	private static TokenIO myInput = new TokenInput();

	/**
	 * The singleton <code>TokenIO</code> we will be using for output.
	 */
	private static TokenIO myOutput = new TokenOutput();

	public static void doInput(TokenGroup tokens, Command command) throws ParseException {
		command.doIO(myInput, tokens);
	}

	public static TokenGroup doOutput(Command command) throws ParseException {
		TokenGroup rGroup = new TokenGroup();
		rGroup.add(TokenMap.getCommandToken(command.getType()));
		command.doIO(myOutput, rGroup);
		return rGroup;

	}

	public abstract Command doCommand(TokenGroup parentGroup, Command commandField, IOTarget target) throws ParseException;

	public abstract boolean doConditional(TokenGroup parentGroup, Token conditionalToken, boolean conditionalExpression);

	public abstract <E extends IOTarget> Map<Token, E> doHashOnFirst(TokenGroup parentGroup, Map<Token, E> tokenSet, E exampleObject, TypeClass tokenType) throws ParseException;

	public abstract <E extends Token> Map<E, List<E>> doHashOnFirstToken(TokenGroup parentGroup, Map<E, List<E>> tokenSet, TypeClass firstType, TypeClass remainingTypes) throws ParseException;

	/**
	 * Continues only if more tokens should be read or written to parentGroup.
	 * It accomplishes this by TODO Finsh this!
	 * 
	 * @param parentGroup
	 * @param conditionalExpression
	 * @return True if there are more, false otherwise.
	 */
	public abstract boolean doIfMore(TokenGroup parentGroup, boolean conditionalExpression);

	public abstract int doInteger(TokenGroup parentGroup, int integerField) throws ParseException;

	public abstract <E extends IOTarget> List<E> doList(TokenGroup parentGroup, List<E> list, E exampleObject) throws ParseException;

	public abstract String doText(TokenGroup parentGroup, String stringField) throws ParseException;

	public abstract Token doToken(TokenGroup parentGroup, Token tokenField, TypeClass tokenType) throws ParseException;

	public abstract <E extends Token> List<E> doTokenList(TokenGroup parentGroup, List<E> list, TypeClass tokenType) throws ParseException;

	public abstract TokenGroup subGroup(TokenGroup parentGroup);

	/**
	 * Continues only if the next token in <code>tokens</code> is a group, or
	 * if the conditional expression is true. The conditions expression should
	 * be true if you want a group to be written.
	 * 
	 * @param parentGroup
	 *        The tokens to check if the next one is a group
	 * @param conditionalExpression
	 *        An expression that checks if what coems next should be a group
	 * @return True if we should continue, false otherwise.
	 */
	public abstract boolean doIfGroup(TokenGroup parentGroup, boolean conditionalExpression);

}
